Xcode

Xcode自动管理证书文件模式下更新provisioning profile文件

  1. 添加设备到develop.apple.com
  2. Xcode切到相应的target的General tab下,然后点击(!)按钮展示provisioning profile文件详情,然后拖动左上角的profile图标到终端获取provisioning profile文件名
  3. 打开~/Library/MobileDevice/Provisioning Profiles目录,然后删除相同文件名的provisioning profile文件
  4. Xcode会自动下载最新的provisioning profile文件

Xcode 打包发布

  1. 打包证书
1
2
打包证书最多只有三个,需要根据提示文字选择正确的证书。
若证书创建后一直无法正常使用,重启后即可
  1. 若无法发布正式,可以发布TestFlight版本测试

Xcode 11打包上传时,一直卡在Authenticating with the App Store…

  1. xcode shift+command+k 清除缓存
  2. open ~/Library/Caches/com.apple.amp.itmstransporter/ 删除文件夹内的所有文件
  3. /Applications/Xcode.app/Contents/SharedFrameworks/ContentDeliveryServices.framework/Versions/A/itms/bin/iTMSTransporter 在终端输入 (亲测在这里打开VPN会快很多,下载内容大概60M+)百度云 [wk8u]
  4. 下载完后,重启Xcode

设置推送证书(两种方式

token Authentication

创建APNs推送服务Key。Certificates, Identifiers & Profiles -> Keys -> JPush Auth Key

⚠️下载完成的是一个.p8文件,只允许下载一次,注意保存

证书

Certificates, Identifiers & Profiles -> Identifiers -> 指定Bundle ID -> 勾选并配置 Push Notifications -> Production SSL Certificate -> 下载.cer证书安装

在钥匙串中导出Apple Push Services: Bundle ID.p12;最好设置一下密码

fix Code signing “.framework” failed;IPA processing failed 请使用xcframework重构framework

add as (New Run Script Phase)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# Type a script or drag a script file from your workspace to insert its path.
APP_PATH="${TARGET_BUILD_DIR}/${WRAPPER_NAME}"

# This script loops through the frameworks embedded in the application and

# removes unused architectures.

find "$APP_PATH" -name '*.framework' -type d | while read -r FRAMEWORK

do

FRAMEWORK_EXECUTABLE_NAME=$(defaults read "$FRAMEWORK/Info.plist" CFBundleExecutable)

FRAMEWORK_EXECUTABLE_PATH="$FRAMEWORK/$FRAMEWORK_EXECUTABLE_NAME"

echo "Executable is $FRAMEWORK_EXECUTABLE_PATH"

EXTRACTED_ARCHS=()

for ARCH in $ARCHS

do

echo "Extracting $ARCH from $FRAMEWORK_EXECUTABLE_NAME"

lipo -extract "$ARCH" "$FRAMEWORK_EXECUTABLE_PATH" -o "$FRAMEWORK_EXECUTABLE_PATH-$ARCH"

EXTRACTED_ARCHS+=("$FRAMEWORK_EXECUTABLE_PATH-$ARCH")

done

echo "Merging extracted architectures: ${ARCHS}"

lipo -o "$FRAMEWORK_EXECUTABLE_PATH-merged" -create "${EXTRACTED_ARCHS[@]}"

rm "${EXTRACTED_ARCHS[@]}"

echo "Replacing original executable with thinned version"

rm "$FRAMEWORK_EXECUTABLE_PATH"

mv "$FRAMEWORK_EXECUTABLE_PATH-merged" "$FRAMEWORK_EXECUTABLE_PATH"

done

Xcode配置

修改类的Copyright、类前缀

TARGETS -> General -> File inspector(right panel) -> Project Document 下就有版权信息和前缀修改

修改默认语言为中文

  1. info.plist 添加或者修改Localization native development region值为China localized resources can be mixed值为yes
  2. PROJECT -> info -> 添加简体中文语言包
  3. 右击.xcodeproj显示包内容,修改project.pbxproj中的developmentRegionzh-Hans
  4. 删除英文语言包后若需要再添加,确保中文语言包在首位

Swift 笔记

视图

自适应容器的高度

一个视图,包含n个子视图,如何才能自适应高度呢?

  1. 第一个子视图顶部对齐父视图
  2. 最后一个子视图底部对齐父视图
  3. 其余的子视图顶部依次对其上一个子视图的底部

同理,横向改成左边/右边对齐

类似微信相册的翻转动画

  1. 预览页
1
2
3
4
5
self.navigationController?.pushViewController(vc, animated: false)
// 设置翻页动画为从右边翻上来
if let view = self.navigationController?.view {
UIView.transition(with: view, duration: 1, options: .transitionFlipFromRight, animations: nil)
}
  1. vc页跳回
1
2
3
4
5
6
7
@objc func done() {
// 设置翻转动画为从左边翻上来
if let view = navigationController?.view {
UIView.transition(with: view, duration: 1, options: .transitionFlipFromLeft, animations: nil)
}
self.navigationController?.popViewController(animated: false)
}
  1. 改变动画时背景为黑色
1
2
3
view.backgroundColor = .black
或者
UIApplication.shared.keyWindow?.backgroundColor = .black

CALayer & Core Animation

1
2
3
4
5
6
7
// 点击按钮弹性动画
let animation: CAKeyframeAnimation = CAKeyframeAnimation()
animation.keyPath = "transform.scale"
animation.values = [1.0, 1.3, 0.9, 1.0]
animation.duration = 0.25
animation.calculationMode = kCAAnimationCubic
btn.layer.add(animation, forKey: nil)

UIView 和 XIB 的相关执行顺序

在controller中通过类方法获得一个关联的UIView(playView.swift/.xib)对象,其执行顺序为:

  1. 由 instantiate 方法获得视图对象:let v = nib.instantiate(withOwner: self, options: nil)[0] as! PlayView
  2. 期间 instantiate 方法调用 awakeFromNib() 执行相应的操作,执行完后再接着执行 instantiate 方法后面的步骤
  3. 执行完获得UIView对象的类方法后,在执行 layoutSubviews() 设置视图大小

UIView的setNeedsLayout, layoutIfNeeded 和 layoutSubviews 方法之间的关系解释

  • layoutSubviews在以下情况下会被调用:
  1. init初始化不会触发layoutSubviews
    但是是用initWithFrame 进行初始化时,当rect的值不为CGRectZero时,也会触发
  2. addSubview会触发layoutSubviews
  3. 设置view的Frame会触发layoutSubviews,当然前提是frame的值设置前后发生了变化
  4. 滚动一个UIScrollView会触发layoutSubviews
  5. 旋转Screen会触发父UIView上的layoutSubviews事件
  6. 改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件
  • 刷新子对象布局:
  1. layoutSubviews方法:这个方法,默认没有做任何事情,需要子类进行重写
  2. setNeedsLayout方法: 标记为需要重新布局,异步调用layoutIfNeeded刷新布局,不立即刷新,但layoutSubviews一定会被调用
  3. layoutIfNeeded方法:如果,有需要刷新的标记,立即调用layoutSubviews进行布局(如果没有标记,不会调用layoutSubviews)
  4. 如果要立即刷新,要先调用[view setNeedsLayout],把标记设为需要布局,然后马上调用[view layoutIfNeeded],实现布局

语言国际化

  1. PROJECT -> Info -> Localizations 添加想要适配的语言。选中后去掉stroyboard的勾选去掉
  2. 在工程文件中创建新的文件右键New File -> Resource -> Strings File,取名为Localizable.strings
  3. .strings下会多出两个字文件,分别对应英文和中文的Strings。如果没有,只需在右侧控制栏的inspector->Localization点击本地化即可
  4. 分别编写语言参数(注意:结尾要有分号;
1
2
"hello" = "你好"; //中文Strings
"hello" = "hello"; //英文Strings
  1. 获取参数值
1
NSLocalizedString("hello", comment: "default") //得到hello对应的值
  1. 快捷使用。新建Localizable.swift,声明多语言文字
1
2
3
4
5
6
7
8
9
enum Localizable: String {
case hello

var localized: String {
return NSLocalizedString(rawValue, comment: "")
}
}

print(Localizable.hello.localized)
  1. 若想修改项目显示名称,则创建InfoPlist.strings文件。只需在右侧控制栏的inspector->Localization点击本地化即可
1
CFBundleDisplayName = "project-name";

Xcode 本地化 修改默认的开发语言

  1. PROJECT -> Info -> Localizations添加中文简体语言
  2. Info.plist中查找并设置Localization Native Development Region值为China
  3. 关闭Xcode,右击.xcodeproj显示包内容,以文本形式打开project.pbxproj文件,查找并替换developmentRegion属性值为zh-Hans
  4. (可选)看情况删除knownRegions数组里的字段
  5. 保存后重启Xcode即可看到默认语言变更为中文简体

AVPlayer获取当前时间戳截图

  1. 声明
1
2
3
4
fileprivate var player: AVPlayer!
fileprivate var playerItem: AVPlayerItem?
fileprivate var playerLayer: AVPlayerLayer!
fileprivate var videoOutput: AVPlayerItemVideoOutput = AVPlayerItemVideoOutput()
  1. 初始化播放控制url
1
2
3
playerItem = AVPlayerItem(url: withURL)
playerItem?.add(videoOutput)
player.replaceCurrentItem(with: playerItem)
  1. 获取截图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/// 获取当前时间的截图
func getCurrentImage() ->UIImage?{
let itemTime = player.currentItem?.currentTime()
let pixelBuffers = videoOutput.copyPixelBuffer(forItemTime: itemTime!, itemTimeForDisplay: nil)
guard let pixelBuffer = pixelBuffers else {
return nil
}
let ciImage = CIImage(cvPixelBuffer: pixelBuffer)
let temporaryContext = CIContext()
let videoImage = temporaryContext.createCGImage(ciImage, from: CGRect(x: 0, y: 0, width: CVPixelBufferGetWidth(pixelBuffer), height: CVPixelBufferGetHeight(pixelBuffer)))
guard let image = videoImage else {
return nil
}
return UIImage(cgImage: image)
}

屏幕方向控制:只允许部分横屏(info.plist中只支持竖屏)

  1. AppDelegate是否允许横屏
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
/// 是否锁定竖屏 默认 true
var isBlock = true {
didSet {
if isBlock {
changeOrientation(fullScreen: !isBlock)
}
}
}

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return isBlock ? .portrait : .allButUpsideDown
}


/// 横竖屏切换
func changeOrientation(fullScreen: Bool) {
if fullScreen { //横屏
if UIApplication.shared.statusBarOrientation.isLandscape {
return
}
let resetOrientationTargert = NSNumber(integerLiteral: UIInterfaceOrientation.unknown.rawValue)
UIDevice.current.setValue(resetOrientationTargert, forKey: "orientation")

let orientationTarget = NSNumber(integerLiteral: UIInterfaceOrientation.landscapeRight.rawValue)
UIDevice.current.setValue(orientationTarget, forKey: "orientation")

}else { //竖屏
if UIApplication.shared.statusBarOrientation.isPortrait {
return
}
let resetOrientationTargert = NSNumber(integerLiteral: UIInterfaceOrientation.unknown.rawValue)
UIDevice.current.setValue(resetOrientationTargert, forKey: "orientation")

let orientationTarget = NSNumber(integerLiteral: UIInterfaceOrientation.portrait.rawValue)
UIDevice.current.setValue(orientationTarget, forKey: "orientation")
}
}
  1. 需要横屏的Controller中设置允许横屏
1
2
3
4
5
// viewWillAppear
let delegate = UIApplication.shared.delegate as! AppDelegate
delegate.isBlock = true
// ⚠️控制器结束后需要将横屏禁止(viewDidDisappear)
delegate.isBlock = true
  1. 如需控制优先显示方向,改变排序即可
1
2
3
4
5
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
</array>

监听锁屏/解锁通知

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class AppDelegate {
...

public static let isLockScreenKey = "isLockScreenKey"

func applicationProtectedDataWillBecomeUnavailable(_ application: UIApplication) {
NotificationCenter.default.post(name: .ScreenLockStateChanged, object: nil, userInfo: [AppDelegate.isLockScreenKey: true])
}

func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) {
NotificationCenter.default.post(name: .ScreenLockStateChanged, object: nil, userInfo: [AppDelegate.isLockScreenKey: false])
}

...
}
public extension Notification.Name {
/// 屏幕状态通知
static let ScreenLockStateChanged = Notification.Name("ScreenLockStateChanged")
}

AVPlayer实现视频后台播放

对于实现后台播放视频的方法,我的理解是,iOS 是支持后台播放音频的,而 AVPlayer 在播放视频时,会将图像渲染在 layer 上,因此只要取消图像的渲染,只播放音频,就可以实现后台播放。

  1. info.plist文件中添加权限
1
2
3
4
5
6
7
8
9
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
  1. 在播放控制类中添加监听(从相关的AVPlayer删除AVPlayer(将AVPlayerLayer player属性设置为nil))
1
2
3
4
5
6
7
8
9
10
11
12
func isVideoBackground() {
NotificationCenter.default.addObserver(self, selector: #selector(enterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
}
@objc func enterBackground() {
// _player 是当前播放AVPlayer对象
// _player = nil;
}
@objc func willForeground() {
// temp 是_player的拷贝
// _player = temp;
}

AVPlayer 实现倍速播放

  1. 定义变量rate保存当前倍速
1
2
3
4
5
6
/// 播放速率
open var rate: Float = 1 {
didSet {
queue.rate = rate
}
}
  1. 每当暂停后播放,都需要设置rate,因为暂停/播放都会修改rate值为0/1
1
2
3
4
open func play() {
queue.play()
queue.rate = rate
}

逻辑

NSNotification通知的使用

  1. 方式一:
    1. 自定义通知类型
    1
    let WBUserShouldLoginNotification =WBUserShouldLoginNotification"
    1. 发送通知
    1
    NotificationCenter.default.post(name: NSNotification.Name(rawValue: WBUserShouldLoginNotification)
    1. 注册 通知监听
    1
    NotificationCenter.default.addObserver(self, selector: #selector(notificationMethod(n:)), name: NSNotification.Name(rawValue: WBUserShouldLoginNotification), object: nil)
    1. 处理通知的方法
    1
    2
    3
    @objc fileprivate func notificationMethod(n: Notification){
    print(n)
    }
    1. 注销 通知监听
    1
    NotificationCenter.default.removeObserver(self)
  2. 方式二:更加贴近官方文档
    1. 自定义通知类型
    1
    2
    3
    extension NSNotification.Name {
    public static let XXXNotification = NSNotification.Name("XXXNotification")
    }
    1. 发送通知
    1
    NotificationCenter.default.post(name: .XXXNotification, object: nil)
    1. 注册 通知监听
    1
    NotificationCenter.default.addObserver(self, selector: #selector(notificationMethod(n:)), name: .XXXNotification, object: nil)
    1. 处理通知的方法
    1
    2
    3
    4
    5
    6
    @objc fileprivate func notificationMethod(n: Notification){
    guard let list = n.object as? [Int] else {
    return
    }
    print(n)
    }
    1. 注销 通知监听
    1
    NotificationCenter.default.removeObserver(self)

自定义选择器Selector

  1. 声明Selector
  2. 判断是否绑定选择器responds(to:)
  3. 触发选择器perform(, with: )

extension 也可以添加限定条件,使得只有特定情况实现 protocol 才能获得默认实现

1
2
3
4
5
6
7
extension ForceTouchImagePreviewProtocol where Self: UIView  {
// 只针对 UIView 的默认实现。在这个实现里,self 为 UIView 类型,可以得到相应参数和方法。
}
extension Array where Element :_ArrayType, Element.Generator.Element :Equatable {
// 对 数组的数组 类型的扩展,且数组中的元素是 Equatable
// 其实 Array 不是 protocol。但可以看出对于 extension 的使用,已经不需要区分是 protocol 还是 class, struct, enum
}

OC & Swift 混编

  1. Swift 调用 objc
    1. 桥接文件(ProjectName-Bridging-Header.h):新建OC文件时自动生成,也可以自己新建Header file后,在 Targets–>Build Settings–>Swift Compiler - General–>objc Bridging Header 配置文件路径(ProjectName/ProjectName-Bridging-Header.h)
    2. 使用时只需在桥接文件中导入头文件即可和 swift 一样使用
  2. objc 调用 Swift
    1. 默认桥接文件(ProjectName-Swift.h):在 Targets–>Build Settings–>Swift Compiler - General–>objc Generated Interface Header Name 进行配置,默认文件名是工程名-Swift.h,一般不做改动
    2. 使用时首先在OC文件中要加上 #import “ProjectName-Swift.h”(名字组成:工程名-Swift)
  3. 混编注意事项:
1
2
3
4
5
// 对于需要混编的Swift类添加@objc声明或继承NSObject或NSObject的子类
class TestClass {
..
}
// 如果要在objc类中使用TestClass类,应当将TestClass继承自NSObject或NSObject的子类,否则,引入ProductName-Swift.h之后,程序找不到对应类。
  1. 更多

指针

系统权限设置

常用的权限:

说明 权限 描述
麦克风权限 Privacy - Microphone Usage Description App需要您的同意,才能访问麦克风进行语音识别转换/录制音频,如禁止将无法识别语音转换/录制音频发送
相机权限 Privacy - Camera Usage Description APP需要您的同意,才能访问相机进行拍摄/识别/视频通话,如禁止将无法拍摄/识别图片以及视频通话
保存相册权限 Privacy - Photo Library Additions Usage Description App需要您的同意,才能将照片保存至相册,如禁止将无法保存图片
相册权限 Privacy - Photo Library Usage Description APP需要您的同意,才能访问相册进行选择照片上传/发布信息,如禁止将无法上传选择照片上传/发布信息
通讯录权限 Privacy - Contacts Usage Description APP需要您的同意,才能获取通讯录好友,如禁止将无法查看
蓝牙权限 Privacy - Bluetooth Peripheral Usage Description APP需要您的同意,才能使用蓝牙播放/控制功能
语音转文字权限 Privacy - Speech Recognition Usage Description APP需要您的同意,才能使用语音识别,如禁止将无法语音转文字
日历权限 Privacy - Calendars Usage Description APP需要您的同意,才能添加任务到日历,如禁止将无法在日历中查看任务
定位权限 Privacy - Location When In Use Usage Description APP需要您的同意,才能访问定位位置展示附近信息,如禁止将无法展示附近信息
定位权限 Privacy - Location Always Usage Description APP需要您的同意,才能访问定位位置展示附近信息,如禁止将无法展示附近信息 ⚠️如非必要,不能申请该权限

定位的需要这么写,防止上架被拒。

  • 网络访问权限

App Transport Security Settings -> Allow Arbitrary Loads (true)

获取系统信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// swift 获取app版本号及 ios 版本号
func versionCheck(){
let infoDictionary = Bundle.main.infoDictionary
let appDisplayName = infoDictionary!["CFBundleName"]
let majorVersion = infoDictionary!["CFBundleShortVersionString"] // 主程序版本号
let minorVersion = infoDictionary!["CFBundleVersion"] // build 版本号
let appversion = majorVersion as! String
let iosversion = UIDevice.current.systemVersion // ios 版本
let identifierNumber = UIDevice.current.identifierForVendor // 设备 udid
let deviceName = UIDevice.current.name //获取设备名称
let systemName = UIDevice.current.systemName // 系统设备名称
let model = UIDevice.current.model // 设备型号
let localizedModel = UIDevice.current.localizedModel // 设备区域化型号 如 A1533
print("appDisplayName:\(appDisplayName!)\nmajorVersion:\(majorVersion!)\nminorVersion:\(minorVersion!)\n",
"appversion:\(appversion)\niosversion:\(iosversion)\nidentifierNumber:\(identifierNumber!)\nsystemName:\(systemName)\nmodel:\(model)\nlocalizedModel:\(localizedModel)")
}

获取当前Wi-Fi名

在iOS 12 之后需要设置Xcode -> [Project Name] -> Targets -> [Target Name] -> Capabilities -> Access WiFi Information -> ON

在iOS 13 以后获取Wi-Fi需要满足以下条件之一

  • 使用定位功能,并且获得了定位服务权限的应用;
  • 使用NEHotspotConfiguration配置过的Wi-Fi;
  • 应用程序已安装有效的VPN配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
/// 获取当前Wi-Fi名
/// 1. #import <SystemConfiguration/CaptiveNetwork.h>
/// 2. Xcode -> [Project Name] -> Targets -> [Target Name] -> Capabilities -> Access WiFi Information -> ON
/// 3. iOS13以后必须定位
/// ```swift
/// private let manager = CLLocationManager()
/// manager.requestWhenInUseAuthorization()
/// ```
var currentSSID: String {
let interfaces = CNCopySupportedInterfaces()
var ssid = ""
if interfaces != nil {
let interfacesArray = CFBridgingRetain(interfaces) as! Array<AnyObject>
if interfacesArray.count > 0 {
//获取当前使用wifi
for name in interfacesArray {
let interfaceName = name as! CFString
let ussafeInterfaceData = CNCopyCurrentNetworkInfo(interfaceName)
if (ussafeInterfaceData != nil) {
let interfaceData = ussafeInterfaceData as! [String: Any]
ssid = interfaceData["SSID"]! as! String
}
}
}
}
return ssid
}

关于 Swift 中重写 preferredStatusBarStyle 不起作用问题解决方案

  • 首先需要在项目targets中的 info.plist 文件中将 View controller-based status bar appearance 删除;
  • 如果UIViewController是存在于自定义的UINavigationController中,需要在自定义的UINavigationController中重写childForStatusBarHiddenchildForStatusBarStyle两个方法: 因为这两个方法默认返回值是nil
1
2
3
4
5
6
7
8
// 重写这两个方法
override var childForStatusBarHidden: UIViewController? {
return self.topViewController
}

override var childForStatusBarStyle: UIViewController? {
return self.topViewController
}

TableView

自定义cell

  1. 从xib创建cell
1
2
3
4
5
6
7
8
9
10
11
// 1. 新建一个空的 TestCell.xib 和 TestCell.swift 文件
// 2. 指定xib的 Custom Class 为 TestCell
// 3. 注册xib
table.register(UINib(nibName: "TestCell", bundle: nil), forCellReuseIdentifier: TestCellID)
// 4. nib实例化只会调用
override class func awakeFromNib() {
super.awakeFromNib()
setup()
}
// 5. 在tableView: cellForRowAtIndexPath: 中使用
let cell = tableView.dequeueReusableCell(withIdentifier: TestCellID, for: indexPath) as! TestCell
  1. 纯代码创建cell
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 1. 创建 TestCell.swift
class TestCell: UITableViewCell {

static func withTableView(_ tableView: UITableView, reuseIdentifier: String? = nil) -> TestCell {
let identifier = reuseIdentifier ?? TestCell.self.description()
var cell = tableView.dequeueReusableCell(withIdentifier: identifier) as? TestCell
if cell == nil {
cell = TestCell(style: .default, reuseIdentifier: identifier)
}
return cell!
}

// 不使用nib实例化只会调用
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier)
setup()
}

required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
setup()
}
}
// 2. 在tableView: cellForRowAtIndexPath: 中使用
let cell = TestCell.withTableView(tableView)
  1. 设置ui
1
2
3
4
5
func setup() {
backgroundColor = UIColor.clear
contentView.backgroundColor = UIColor.clear
selectionStyle = .none
}

属性问题

  • table.tableFooterView = UIView() 去除多余的分割线
  • table.separatorStyle = .none 隐藏分割线

自定义xib大小无法改变时

Attributes inspector -> Simulated metric: Size:inferred -> Size:Freeform

tableview是否注册cell

如果不想手动初始化cell实例,则注册class/nib,使用注册时需要携带indexPath参数

  1. 基于class的注册,使用registerClass方法
1
2
3
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "identifier")
// 在tableView: cellForRowAtIndexPath: 中使用
tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath)
  1. 基于nib的注册,使用registerNib方法
1
2
3
tableView.register(UINib(nibName: "TestCell", bundle: nil), forCellReuseIdentifier: "identifier")
// 在tableView: cellForRowAtIndexPath: 中使用
tableView.dequeueReusableCell(withIdentifier: "identifier", for: indexPath)
  1. 基于class的不注册,手动判别cell是否为空
1
2
3
4
5
// 在tableView: cellForRowAtIndexPath: 中使用
var cell = tableView.dequeueReusableCell(withIdentifier: "identifier") as? TestCell
if cell == nil {
cell = TestCell(style: .default, reuseIdentifier: "identifier")
}
  1. 基于nib的不注册,手动判别cell是否为空
1
2
3
4
5
// 在tableView: cellForRowAtIndexPath: 中使用
var cell = tableView.dequeueReusableCell(withIdentifier: "identifier") as? TestCell
if cell == nil {
cell = Bundle.main.loadNibNamed("TestCell", owner: self, options: nil)?.last as? TestCell
}

TableViewCell的性能优化

1
2
3
4
5
6
7
8
// 离屏渲染 - 异步绘制
self.layer.drawsAsynchronously = true
// 栅格化 - 异步绘制之后,会生成一张独立的图像,cell在滚动的时候,本质上滚动的是这张图片
// cell优化,要尽量减少图层数量,相当于就只有一层
// 停止滚动之后,可以监听
self.layer.shouldRasterize = true
// 使用 ’珊格化‘需要指定分辨率,否则图像不清晰
self.layer.rasterizationScale = UIScreen.main.scale
  • 优点:绘制cell更快,更高效
  • 缺点:离屏渲染需要在 GPU/CPU 之间快速切换
    更加耗电

关闭导航栏自动缩进

1
2
3
4
5
if #available(iOS 11.0, *) {
scroll.contentInsetAdjustmentBehavior = .never
} else {
automaticallyAdjustsScrollViewInsets = false
}

TableViewCell 设置间距,去除顶部间距

  1. tableview 初始化estimated必须在view设置之前
1
2
3
4
let table = UITableView(frame: .zero, style: .grouped)
table.estimatedSectionHeaderHeight = 0
table.estimatedSectionFooterHeight = 0
table.tableFooterView = UIView()
  1. 代理设置间距
1
2
3
4
5
6
func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return 0.1
}
func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
return 10
}

tableview 关闭延时点击

table.delaysContentTouches = false

UIScrollView 自动布局约束滚动

  1. 添加一个scroll view

  2. 像普通视图一样为scroll view添加位置和大小的约束

  3. 在scroll view中添加一个子视图(content view),给该视图添加一个指定的标签(这个标签只是为了更好地显示)

  4. 将content view的left,right,top,bottom和scroll view的边界建立相等约束。那么现在content view的边界就确定了scroll view的内容区域

(注意此时content view还没有固定的大小,它可以根据你在其中设置的视图的伸缩大小)

  1. (可选)如果不需要水平滑动,将content view的宽度设置为和scoll view的宽度相等。

  2. (可选)如果不需要垂直滑动,将content view的高度设置为和scroll view的高度相等。

在content view中添加子视图,为子视图和content view添加约束。

重要: 你的布局必须能够决定content view的大小(除非在5和6中已经设置过了)。如果要基于你的内容的固有尺寸来决定高度,那么在content view的top跟bottom之间必须有一条不间断的约束链。类似地,对于宽度,必须要在left和right间有不间断的约束链。如果你在content view中添加的内容(子视图)不具有固有尺寸,那么你要显式地为content view或者其内容确定好合适的尺寸。当content view的高度大于scroll view的高度,那么scroll view支持垂直方向的滑动。当content view的宽度大于scroll view的宽度,那么scoll view支持水平方向上的滑动。否则,默认滑动是被禁止的。

iOS UITableViewCell 中 调整imageView 的图片大小

1
2
3
4
5
6
7
8
9
10
11
12
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
...

// 修改image大小 ⚠️若是网络图片必须在图片加载完成后再一次调用
let itemSize = CGSize(width: 50, height: 50)
UIGraphicsBeginImageContextWithOptions(itemSize, _: false, _: UIScreen.main.scale)
let imageRect = CGRect(x: 0.0, y: 0.0, width: itemSize.width, height: itemSize.height)
cell.imageView?.image?.draw(in: imageRect)
cell.imageView?.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
return cell
}

UITableViewCell的分割线边距设置

1
2
3
4
5
6
7
8
9
10
11
12
func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
if cell.responds(to: #selector(setter: UITableView.separatorInset)) {
cell.separatorInset = UIEdgeInsets(top: 0, left: 75, bottom: 0, right: 0)
if indexPath.row == (conversationModel.dataList.count - 1) {
cell.separatorInset = .zero
}
}

}

// responds(to: selector) 判断是否响应Selector
// perform(selector, with: target) 调用Selector并传递第一个参数

上架

备注说明

  1. 如果有使用VoIP,需要提供演示视频说明
  2. 如果有使用定位/后台相关功能,也需要视频演示
  3. 如果有内购,需要提供内购账号密码,并加以描述CNY 699元 是购买684金币
  4. 苹果登陆需要直接点击就可以使用app;若需要用户完善信息的,则需要提供能完整使用功能的测试账号
  5. 有金币等交易相关的商品,需要加入内购;可以只提供两个价格,一个最低,一个最高

注:以上视频需要拼接为一个,以附件形式上传,最好200M以内

Regarding Guideline 1.2 - Safety - User Generated Content

反馈描述:

1
对于用户上传的信息,我们有服务端的数据风控策略,会自动隐藏/封闭用户数据, 不需要做拉黑描述等

如果app内部有投诉/举报相关功能,最好提供演示视频以说明

xcode 构建版本被删除

  1. 隐私权限没有在info.plist里进行声明(⚠️查收appleid邮箱邮件
  2. 使用了Apple禁止使用的API
  3. 重新生成打包证书,retry!